package xpath

import (
	"os"
	"regexp"
	"strings"
	"unicode/utf8"
)

func max(a int, b int) int {
	if a > b {
		return a
	}
	return b
}

func index(s string, sub string, start int) int {
	if start > len(s) { return -1 }
	var index = strings.Index(s[start:], sub)
	if index == -1 {
		return -1
	} else {
		return index + start
	}
}

func peek(s string, pos int ) rune {
	if pos >= len(s) {
		return 0
	}
	return rune(s[pos])
}

var DirectorySeparator = "/"
func FileExtensionIs(path string, extension string) bool {
	var pathLen = len(path)
	var extLen = len(extension)
	return pathLen > extLen && path[pathLen - extLen:pathLen] == extension
}

func GetRootLength(path string) int {
	if peek(path, 0) == '/' {
		if peek(path, 1) != '/' {
			return 1
		}
		var p1 = index(path, "/", 2)
		if p1 < 0  { return 2 }
		var p2 = index(path, "/", p1+1)
		if p2 < 0 { return p1 + 1 }
		return p2 + 1
	}
	if peek(path, 1) == ':' {
		if peek(path, 2) == '/' { return 3 }
		return 2
	}
	return 0
}

// Get Directory Not Trailing Separator
func GetDirectoryPath(path string) string {
	path = NormalizePath(path)
	return path[: max(GetRootLength(path), strings.LastIndex(path, DirectorySeparator))]
}

func IsUrl(path string) bool {
	return len(path) > 0 && !IsRootedDiskPath(path) && strings.Index(path, "://") != -1
}

func IsRootedDiskPath(path string) bool {
	return GetRootLength(path) != 0
}

func ConvertToRelativePath(absoluteOrRelativePath string, basePath string, getCanonicalFileName func (path string) string) string {
	if  !IsRootedDiskPath(absoluteOrRelativePath) {
		return absoluteOrRelativePath
	}

	return GetRelativePathToDirectoryOrUrl(basePath, absoluteOrRelativePath, basePath, getCanonicalFileName, false)
}

func NormalizeSlashes(path string) string {
	return regexp.MustCompile(`\\`).ReplaceAllString(path, "/")
}

// ../ in here is valid
func getNormalizedParts(normalizedSlashedPath string, rootLength int) []string {

	var parts = strings.Split(normalizedSlashedPath[rootLength:], DirectorySeparator)
	var normalized []string
	for i := 0; i < len(parts); i++ {
		var part = parts[i]
		if part != "." {
			if part == ".." && len(normalized) > 0 && normalized[len(normalized)- 1] != ".." {
				normalized = normalized[:len(normalized)-1]
			} else {
				normalized = append(normalized, part)
			}
		}
	}

	return normalized
}

func NormalizePath(path string) string {
	path = NormalizeSlashes(path)
	var rootLength = GetRootLength(path)
	var normalized = getNormalizedParts(path, rootLength)
	return path[:rootLength] + strings.Join(normalized, DirectorySeparator)
}

func GetBaseFilename(path string) string {
	path = NormalizePath(path)
	var i = strings.LastIndex(path, DirectorySeparator)
	if i < 0 {
		return path
	} else {
		return path[i+1:]
	}
}

func GetBaseFileNameNoExtension(path string) string{
	var result = GetBaseFilename(path)
	var index = strings.Index(result, ".")
	if index >= 0 {
		return result[:index]
	}
	return result
}

func CombinePaths(path1 string, path2 string) string {
	path1 = NormalizePath(path1)
	path2 = NormalizePath(path2)

	var runes1 = []rune(path1)
	var runes2 = []rune(path2)

	if len(runes1) == 0 { return path2 }
	if len(runes2) == 0 { return path1 }
	if string(runes1) == DirectorySeparator { return path2 }
	if string(runes1[len(runes1)-1]) == DirectorySeparator { return path1 + path2 }
	return path1 + DirectorySeparator + path2
}

func normalizedPathComponents(path string, rootLength int) []string {
	var normalizedParts = getNormalizedParts(path, rootLength)
	var result []string
	result = append(result, path[:rootLength])
	result = append(result, normalizedParts...)
	return result
}

func GetNormalizedPathComponents(path string, currentDirectory string) []string {
	path = NormalizeSlashes(path)
	var rootLength = GetRootLength(path)
	if rootLength == 0 {
		// If the path is not rooted it is relative to current directory
		path = CombinePaths(NormalizeSlashes(currentDirectory), path)
		rootLength = GetRootLength(path)
	}
	return normalizedPathComponents(path, rootLength)
}

func GetNormalizedAbsolutePath(fileName string, currentDirectory string) string {
	return GetNormalizedPathFromPathComponents(GetNormalizedPathComponents(fileName, currentDirectory))
}

func GetNormalizedPathFromPathComponents(pathComponents []string) string {
	if len(pathComponents) > 0 {
		return pathComponents[0] + strings.Join(pathComponents[1:], DirectorySeparator)
	}
	return ""
}

func GetNormalizedPathComponentsOfUrl(url string) []string {
	// Get root length of http://www.website.com/folder1/foler2/
	// In this example the root is:  http://www.website.com/
	// normalized path components should be ["http://www.website.com/", "folder1", "folder2"]

	var urlLength = len(url)
	// Initial root length is http:// part

	var rootLength = strings.Index(url, "://") + len("://")
	for rootLength < urlLength {
		// Consume all immediate slashes in the protocol
		// eg.initial root length is just file:// but it needs to consume another "/" in file:///
		r, s := utf8.DecodeRuneInString(url[rootLength:])
		if r == '/' {
			rootLength += s
		} else {
			// non slash character means we continue proceeding to next component of root search
			break
		}
	}

	// there are no parts after http:// just return current string as the pathComponent
	if rootLength == urlLength {
		return []string{url}
	}

	// Find the index of "/" after website.com so the root can be http://www.website.com/ (from existing http://)
	var indexOfNextSlash = index(url, DirectorySeparator, rootLength)
	if indexOfNextSlash != -1 {
		// Found the "/" after the website.com so the root is length of http://www.website.com/
		// and get components afetr the root normally like any other folder components
		rootLength = indexOfNextSlash + 1
		return normalizedPathComponents(url, rootLength)
	} else {
		// Can't find the host assume the rest of the string as component
		// but make sure we append "/"  to it as root is not joined using "/"
		// eg. if url passed in was http://website.com we want to use root as [http://website.com/]
		// so that other path manipulations will be correct and it can be merged with relative paths correctly
		return []string{url + DirectorySeparator}
	}
}

func GetNormalizedPathOrUrlComponents(pathOrUrl string, currentDirectory string) []string {
	if IsUrl(pathOrUrl) {
		return GetNormalizedPathComponentsOfUrl(pathOrUrl)
	} else {
		return GetNormalizedPathComponents(pathOrUrl, currentDirectory);
	}
}

func GetRelativePathToDirectoryOrUrl(directoryPathOrUrl string, relativeOrAbsolutePath string, currentDirectory string, getCanonicalFileName func (fileName string) string, isAbsolutePathAnUrl bool) string {
	var pathComponents = GetNormalizedPathOrUrlComponents(relativeOrAbsolutePath, currentDirectory);
	var directoryComponents = GetNormalizedPathOrUrlComponents(directoryPathOrUrl, currentDirectory);
	if len(directoryComponents) > 1 && directoryComponents[len(directoryComponents)- 1] == "" {
		// If the directory path given was of type test/cases/ then we really need components of directry to be only till its name
		// that is  ["test", "cases", ""] needs to be actually ["test", "cases"]
		directoryComponents = directoryComponents[:len(directoryComponents)-1]
	}

	// Find the component that differs
	var joinStartIndex = 0
	for ; joinStartIndex < len(pathComponents) && joinStartIndex < len(directoryComponents); joinStartIndex++ {
		if getCanonicalFileName(directoryComponents[joinStartIndex]) != getCanonicalFileName(pathComponents[joinStartIndex]) {
			break
		}
	}

	// Get the relative path
	if joinStartIndex != 0 {
		var relativePath = ""
		var relativePathComponents = pathComponents[joinStartIndex:]

		for ; joinStartIndex < len(directoryComponents); joinStartIndex++ {
			if directoryComponents[joinStartIndex] != "" {
				relativePath = relativePath + ".." + DirectorySeparator
			}
		}

		return relativePath + strings.Join(relativePathComponents, DirectorySeparator)
	}

	// Cant find the relative path, get the absolute path
	var absolutePath = GetNormalizedPathFromPathComponents(pathComponents)
	if isAbsolutePathAnUrl && IsRootedDiskPath(absolutePath) {
		absolutePath = "file:///" + absolutePath
	}

	return absolutePath
}

func PathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if err == nil {
		return true, nil
	}
	if os.IsNotExist(err) {
		return false, nil
	}
	return false, err
}

func PathExistsNoErr(path string) bool {
	var result, _ = PathExists(path)
	return result
}